package edu.kit.ksri.db.mturk.plugins.display;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.lang.ArrayUtils;
import org.apache.log4j.Logger;

import com.amazonaws.mturk.requester.Assignment;
import com.amazonaws.mturk.requester.HIT;

import edu.kit.ksri.db.mturk.plugins.correctness.answers.AbstractAssignmentAnswer;
import edu.kit.ksri.db.mturk.utils.messaging.Result;
import edu.kit.ksri.db.mturk.var.ConnectionMode;
import edu.kit.ksri.db.mturk.var.Icd;
import edu.kit.ksri.db.mturk.var.StaticInformation;

/**
 * Copyright (c) 2010-2011 Karlsruhe Institute of Technology (KIT), Germany
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 * 
 * --------------------------------------------------------------------------
 * 
 * Software: CSP/WMV tool for dynamic quality management of microtasks
 * http://www.ksri.kit.edu/Upload/Users/PeopleClouds/HCOMP2011_An_extendable_toolkit_for_managing_quality_of_human-based_eServices.pdf
 * 
 * Organization: Karlsruhe Institute of Technology (KIT), Germany
 * http://www.kit.edu
 * 
 * Project: PeopleClouds project of the Karlsruhe Service Research Institute(KSRI)
 * http://www.ksri.kit.edu/Default.aspx?PageId=818
 * 
 * Contributing authors: David Bermbach, Sandra Rath, Pascal Wichmann, Robert Kern
 * 
 * ---------------------------------------------------------------------------
 * 
 * Purpose: This is the live demo monitoring tool. Suggested use: for demo purposes only.
 *          The monitoring output file can be found in the output folder and is a HTML file.
 */
public class BrowserDisplay {
	
	private static final Logger basiclog = Logger.getLogger("basic." + BrowserDisplay.class);
	
	// All the rows of the table
	private static BrowserDisplayRow[] rows = null;
	
	// All HIT IDs
	private static String[] hitids = null;
	
	// All tasks from the scenario input file
	private static String[] tasks = null;
	
	// All the IDs of the working workers
	private static HashSet<String> workerids = new HashSet<String>();
	
	// Flags for setting the status
	private static String ongoingFlag = "Ongoing";
	private static String rawFlag = "Raw";
	private static String newFlag = "New";
	private static String escalatedFlag = "Escalated";
	private static String notInspectedFlag = "Not inspected";
	private static String finalFlag = "Final";
	private static String incompleteFlag = "Incomplete";
	
	// Number of HITs that are new, raw, ongoing, escalated, not inspected, incomplete and final
	private static int totalHitCount = 0;
	private static int newHitCount = 0;
	private static int rawHitCount = 0;
	private static int ongoingHitCount = 0;
	private static int incompleteHitCount = 0;
	private static int escalatedHitCount = 0;
	private static int notInspectedHitCount = 0;
	private static int finalHitCount = 0;
	
	// Number of incoming qualification requests
	private static int qualificationRequestsCount = 0;
	
	// Maximum number of raw results, needed to determine the number of columns
	private static int maxNumberOfRawResults = 0;
	
	// Contains all escalated HITs
	private static Set<String> escalatedHits = new HashSet<String>(); 
	
	// Version number of the monitoring
	private static int versionNumber = 0;
	
	// Splits the answers of one assignment
	private static String answerDelim = " | ";

	/**
	 * All new created HITs, called by StandaloneInitializer once to initialize the rows of the table
	 * 
	 * @param pHits
	 */
	public static void initialize(HIT[] pHits){
		
		// Initialize HIT IDs and the number of HITs total and new
		hitids = new String[pHits.length];
		totalHitCount = pHits.length;
		newHitCount = pHits.length;
		
		// Initialize tasks (e.g. OCR image) and rows of the table
		tasks = new String[pHits.length];
		rows = new BrowserDisplayRow[pHits.length];
		
		// Read the tasks from the scenario input file
		try {
			String inputFile = StaticInformation.inputFile;
			
			if(StaticInformation.scenario.getConnectionMode() == ConnectionMode.CORRSIM)
				inputFile = StaticInformation.siminputFile;
			else if(StaticInformation.scenario.getConnectionMode() == ConnectionMode.CORRSIM)
				inputFile = StaticInformation.siminputFile;

			BufferedReader br = new BufferedReader(new FileReader(inputFile));
			
			String line = br.readLine();
			
			int i = -1;
			while (line != null) {
				if(i == -1){
					i++;
					line = br.readLine();
					continue;
				}

				tasks[i] = line;
				line = br.readLine();
				i++;
			}
		} catch(ArrayIndexOutOfBoundsException e){
			basiclog.error("Check that the inputFile is corresponding to the HITs");
		} catch (FileNotFoundException e) {
			basiclog.error("Couldn't read input file for tasks/urls");
		} catch (IOException e) {
			basiclog.error("Couldn't read input file for tasks/urls");
		}
		
		// Initialize the rows/HITs in the table and save the HIT IDs
		for(int i = 0; i < pHits.length; i++){
			HIT h = pHits[i];
			rows[i] = new BrowserDisplayRow((i+1), h.getHITId(), tasks[i], newFlag);
			hitids[i] = pHits[i].getHITId();
		}
		
		update();
		basiclog.info("Monitoring is initialized.");
	}
	
	/**
	 * Called by QM after every decision (e.g. WMV), updates the status of the HITs/rows
	 * 
	 * @param results
	 */
	public static void update(Map<String, Result> results){
		
		// For every result
		for(Result r : results.values()){
			// Get the index of the current HIT to find it in the row array
			int index = ArrayUtils.indexOf(hitids,r.getHitId());

			// The correct result was found
			if(r.isFinalResult()){

				// Decrease the number of ongoing HITs, since we found the correct result
				if(rows[index].getStatus().equals(ongoingFlag)){
					ongoingHitCount--;
				}
				
				// Decrease the number of raw HITs, since we found the correct result
				if(rows[index].getStatus().equals(rawFlag)){
					rawHitCount--;
				}

				// If the last status of the result wasn't "correct", do this
				if(!(rows[index].getStatus().equals(finalFlag) || rows[index].getStatus().equals(notInspectedFlag))){
					
					// Get every answer
					Set<Entry<String, String>> s = AbstractAssignmentAnswer.parseAnswer(r.getCorrectSolution().getRawAnswer()).entrySet();
					
					String answers = "";
					
					for(Entry<String, String> e : s){
						if(answers.equals(""))
							answers = e.getValue();
						else
							answers = answers + answerDelim + e.getValue();
						
					}
					
					// Set correct answer
					rows[index].setCorrectAnswer(answers);
					
					// Increase the number of not inspected HITs or of final HITs
					if(r.getPhiCorrect() == -1){//Not inspected
						rows[index].setStatus(notInspectedFlag);
						notInspectedHitCount++;
					}
					else{//Final
						rows[index].setStatus(finalFlag);
						finalHitCount++;
					}
					
					update();
				}
			}
			else{// If result is not correct
				// Decrease raw HIT count
				if(rows[index].getStatus().equals(rawFlag)){
					rawHitCount--;
				}
				
				// If HIT was escalated before, it can't be de-escalated, same for incomplete and final
				if(rows[index].getStatus().equals(escalatedFlag) || rows[index].getStatus().equals(incompleteFlag) || rows[index].getStatus().equals(finalFlag)){
					continue;
				}
				
				// Increase ongoing HIT count and set the ongoing status
				if(!rows[index].getStatus().equals(ongoingFlag)){
					rows[index].setStatus(ongoingFlag);
					ongoingHitCount++;
					
					//Only update if the status of the result is not already ongoing. 
					//If it is already ongoing, we don't need an update.
					update();
				}
			}
			
		}
	}
	
	/**
	 * Updates incoming raw results, called by ResultQueue, whenever new results are available
	 * 
	 * @param r
	 */
	public static void update(Result r){

		// Get the index of the current HIT to find it in the row array
		int index = ArrayUtils.indexOf(hitids,r.getHitId());
		
		// Get all the assignments of the result
		List<Assignment> assignments = r.getAssignments();
		
		String answers = "";
		
		// Do for each assignment
		for(int i = 0; i < assignments.size(); i++){
			// Get one assignment
			Assignment a = assignments.get(i);
			
			// Add the workerid to the worker set in order to determine the number of working workers
			workerids.add(a.getWorkerId());

			if(StaticInformation.scenario instanceof Icd){
				// Parse the answer
				HashMap<String, String> answersMap = AbstractAssignmentAnswer.parseAnswer(a.getAnswer());
				
				String comment = answersMap.get("comment");
				int answersCount;
				
				if(comment == null){
					answersCount = (answersMap.size());
				}
				else{
					answersCount = (answersMap.size()-1);
					comment = comment.replace("\r", " ");
				}
				
				int labelCount = answersCount / (Icd.flagCount + 1);

				for(int j = 1; j <= labelCount; j++){
					
					String code = answersMap.get("code"+String.valueOf(j));
					String flag1 = answersMap.get("code"+String.valueOf(j)+"flag1");
					String flag2 = answersMap.get("code"+String.valueOf(j)+"flag2");
					String flag3 = answersMap.get("code"+String.valueOf(j)+"flag3");
					String flag4 = answersMap.get("code"+String.valueOf(j)+"flag4");
					
					//change false|true answers to true
					if (flag1.contains("true")) {
						flag1 = "true";				
					}
					if (flag2.contains("true")) {
						flag2 = "true";				
					}
					if (flag3.contains("true")) {
						flag3 = "true";				
					}
					if (flag4.contains("true")) {
						flag4 = "true";				
					}

					String label = code+" "+flag1+" "+flag2+" "+flag3+" "+flag4;
					label = label.trim();
					if(label.equals("false false false false")){
						//Do nothing
					}
					else{
						// Separate the answers of the same assignment
						String answerPartDelim = ""; 
						if(!answers.equals("")){
							answerPartDelim = ", ";
						}
						
						// Save the assignments till this one assignment is done
						answers = answers + answerPartDelim + label;
					}
				}
				// Add the answer to the row/HIT
				rows[index].addAnswer(answers);
				
				// Determine the maximum number of raw results in order to know how many columns are needed in the table
				maxNumberOfRawResults = Math.max(maxNumberOfRawResults, rows[index].getAnswers().size());
			}
			else{
				//Get all the answers for one assignment
				Set<Entry<String, String>> s = AbstractAssignmentAnswer.parseAnswer(a.getAnswer()).entrySet();
				
				String answer = "";
				
				//Concatenate the answers of the same assignment
				for(Entry<String, String> e : s){
					if(answers.equals(""))
						answer = e.getValue();
					else
						answer = answer + answerDelim + e.getValue();
					
				}
				
				// Add the answer to the row/HIT
				rows[index].addAnswer(answer);
				
				// Determine the maximum number of raw results in order to know how many columns are needed in the table
				maxNumberOfRawResults = Math.max(maxNumberOfRawResults, rows[index].getAnswers().size());
			}
		}//		
		
		// Decrease/increase the new/raw HIT counts and set the status of the HIT/row
		if(rows[index].getStatus() != null){
			//Do only adjust the HIT counts and set the status when the previous status was "new"
			if(rows[index].getStatus().equals(newFlag)){
				rows[index].setStatus(rawFlag);
				rawHitCount++;
				newHitCount--;
			}
		}
		
		// Only updating when we actually have new assignments
		if(assignments.size() != 0){
			update();
		}
	}
	
	/**
	 * Updates the escalated HITs, called by ResultOutputCollector once a HIT is escalated
	 * 
	 * @param pEscalatedHits
	 */
	public static void update(Set<String> pEscalatedHits){
		// Add all escalated HITs
		escalatedHits.addAll(pEscalatedHits);
		
		for (String s : pEscalatedHits){
			// Get the index of the current HIT to find it in the row array
			int index = ArrayUtils.indexOf(hitids,s);
			
			// Previous status was raw, so decrease the raw HIT count
			if(rows[index].getStatus().equals(rawFlag)){
				rawHitCount--;
			}
			
			// Previous status was ongoing, so decrease the ongoing HIT count
			if(rows[index].getStatus().equals(ongoingFlag)){
				ongoingHitCount--;
			}
			
			// Only set the status and increase the escalated HIT count when the HIT wasn't escalated before
			if(!rows[index].getStatus().equals(escalatedFlag)){
				rows[index].setStatus(escalatedFlag);
				escalatedHitCount++;
			}
		}
		
		update();
	}
	
	/**
	 * Sets the status of the unfinished HITs to incomplete when the run is done
	 */
	public static void finish(){
		
		for(BrowserDisplayRow bdr : rows){
			
			String status = bdr.getStatus();
			
			if(!(status.equals(finalFlag) || status.equals(escalatedFlag) || status.equals(notInspectedFlag) || status.equals(incompleteFlag))){
				
				if(status.equals(rawFlag))
					rawHitCount--;
				
				if(status.equals(newFlag))
					newHitCount--;
				
				if(status.equals(ongoingFlag))
					ongoingHitCount--;
				
				bdr.setStatus(incompleteFlag);
				incompleteHitCount++;
			}
		}
		
		update();
	}
	
	/**
	 * Is called when a single HIT is incomplete
	 * @param hit
	 */
	public static void update(String hit){
		// Get the index of the current HIT to find it in the row array
		int index = ArrayUtils.indexOf(hitids,hit);
		
		String status = rows[index].getStatus();
		
		if(status.equals(incompleteFlag)){
			basiclog.error("It was tried to set status of hit " + hit + " to incomplete, but was already incomplete.");
			return;
		}

		if(!(status.equals(finalFlag) || status.equals(escalatedFlag) || status.equals(notInspectedFlag) || status.equals(incompleteFlag))){
			
			if(status.equals(rawFlag))
				rawHitCount--;
			
			if(status.equals(newFlag))
				newHitCount--;
			
			if(status.equals(ongoingFlag))
				ongoingHitCount--;
			
			rows[index].setStatus(incompleteFlag);
			incompleteHitCount++;
		}
		else{
			basiclog.error("It was tried to set status of hit " + hit + " to incomplete, but was already " + status);
		}
		
		update();
	}
	
	/**
	 * Returns the Header with the Summary information of the HTML page
	 * 
	 * @return Header
	 */
	private static String getHeader(){
		
		String automaticRefresh = "";
		
		if(StaticInformation.scenario.getRefreshDelay() != 0){
			automaticRefresh = "<meta http-equiv=\"refresh\" content=\"" + StaticInformation.scenario.getRefreshDelay() +"; URL=summary.html\">";
		}
		
		String actualHeader = "<html><head>" + automaticRefresh + "</head><body><table>";
		
		String logo = "<tr><td><img src=\"../../../ksrilogo.png\"><br/><br/></td></tr>";
		
		String summary = "<tr><td><b>MTurkTool Live Summary:</b> Total HITs: " + totalHitCount + ", New: " + newHitCount + ", Raw: " + rawHitCount 
			+ ", Ongoing: " + ongoingHitCount + ", Escalated: " + escalatedHitCount + ", Final: " + finalHitCount + ", Not inspected: " + notInspectedHitCount + ", Incomplete: " + incompleteHitCount
			+ "<br/>Qualification Requests: " + qualificationRequestsCount + ", Working workers: " + workerids.size()
			+ "<br/>Version: " + versionNumber + "<br/><br/></td></tr>";
		
		String footer = "</table><br/>";
		
		
		
		return actualHeader + logo + summary + footer;
	}
	
	/**
	 * Called by QMan, updates the number of qualification requests
	 * @param numberOfQRequests
	 */
	public static void update(int numberOfQRequests){
		qualificationRequestsCount+=numberOfQRequests;	
		
		update();
	}
	
	/**
	 * Returns the Bottom of the HTML page
	 * 
	 * @return Bottom
	 */
	private static String getBottom(){
		return "</body></html>";
	}
	
	/**
	 * The real update function that prints the HTML page, is called internally when any update should be done
	 */
	private static void update(){

		try {
			// Initialize the to be printed HTML page String
			String printString = "";
			
			// Increase version number
			versionNumber++;
			
			// If the parameter is -1, then always show the table
			if(StaticInformation.showOnlySummaryWhenMoreThanResults == -1){
				printString+= getHeader() + getTable() + getBottom();
			}
			// If the parameter is 0, then never show the table
			else if(StaticInformation.showOnlySummaryWhenMoreThanResults == 0){
				printString+= getHeader() + getBottom();
			}
			// If the number of received results is bigger than the parameter, then only show the summary
			//totalHitCount-newHitCount = number of results received
			else if((totalHitCount-newHitCount) > StaticInformation.showOnlySummaryWhenMoreThanResults){
				printString+= getHeader() + getBottom();
			}
			// Show the table
			else{
				printString+= getHeader() + getTable() + getBottom();
			}
			
			// Declare the PrintWriter
			PrintWriter pw;
			
			// Figure out the correct folder to save the HTML page at
			if(StaticInformation.scenario.getConnectionMode() == ConnectionMode.CORRSIM){
				pw = new PrintWriter(StaticInformation.simMTurkOutputDirectory + "/summary.html");
			}
			else if(StaticInformation.scenario.getConnectionMode() == ConnectionMode.CORRSIMINPUT){
				pw = new PrintWriter(StaticInformation.simMTurkInputDirectory + "/summary.html");
			}
			else{
				pw = new PrintWriter(StaticInformation.resultOutputDirectory + "/summary.html");
			}

			// Print the HTML page
			pw.println(printString);

			// Clean up
			pw.flush();
			pw.close();
			
		} catch (FileNotFoundException e) {
			basiclog.error("Error while monitoring results: " + e);
		}
	}
	
	/**
	 * Returns the table information with all its rows
	 * 
	 * @return Table
	 */
	private static String getTable(){

			// Initialize often used HTML tags
			String rowPrefix = "<tr><td>";
			String rowSuffix = "</td></tr>";
			String colSeparator = "</td><td>";
			
			// Header of the table
			String table = "<table border=1><tr><td>#</td><td>Task</td><td>Result</td><td>Status</td>";
			
			for(int i = 1; i <= maxNumberOfRawResults; i++){
				table = table + "<td>Raw " + i + "</td>";
			}

			table = table + "</tr>";
			
			// Print all the rows
			for(BrowserDisplayRow bdr : rows){
				
				// If we don't wanna show new HITs in monitoring and the current HIT is new
				if(!StaticInformation.showNewHITsInMonitoring && bdr.getStatus().equals(newFlag))
					continue;
				
				// Print Mapping number
				table = table + rowPrefix  + bdr.getMappingNumber() + colSeparator;
				
				// Print Task
				if(StaticInformation.scenario.getTaskInputType().equals("TEXT")){
					table = table + bdr.getTask();
				}
				else if(StaticInformation.scenario.getTaskInputType().equals("URL")){
					table = table + "<a href=\"" + bdr.getTask() + "\">" + bdr.getTask() + "</a>";
				}
				else if(StaticInformation.scenario.getTaskInputType().equals("IMG")){
					table = table + "<img src=\"" + bdr.getTask() + "\" height=" + StaticInformation.scenario.getTaskPictureHeight() + ">";
				}
				else if(StaticInformation.scenario.getTaskInputType().equals("IMGs")){
					String pictures = bdr.getTask();
					String[] pictureArray = pictures.split("\t");
					
					// Several pictures are shown, e.g. Poker cards
					for(String s : pictureArray){
						table = table + "<img src=\"" + s + "\" height=" + StaticInformation.scenario.getTaskPictureHeight() + ">";
					}
				}
				
				table = table + colSeparator;

				// Get Status
				String status = bdr.getStatus();
				
				// Print Result
				if(status.equals(finalFlag) || status.equals(notInspectedFlag)){
					//Only print correct result when answer is available
					table = table + bdr.getCorrectAnswer() + "</td>";
				}
				else{
					table = table + "</td>";
				}
				
				// Print Status				
				if(status.equals(finalFlag)){
					table = table + "<td bgcolor=\"#339900\">Final" + colSeparator;
				}
				else if(status.equals(escalatedFlag)){
					table = table + "<td bgcolor=\"#FF3300\">Escalated" + colSeparator;
				}
				else if(status.equals(incompleteFlag)){
					table = table + "<td bgcolor=\"#FF3300\">Incomplete" + colSeparator;
				}
				else if(status.equals(ongoingFlag)){
					table = table + "<td bgcolor=\"#FF9900\">Ongoing" + colSeparator;
				}
				else if(status.equals(newFlag)){
					table = table + "<td bgcolor=\"#FFFFFF\">New" + colSeparator;
				}
				else if(status.equals(rawFlag)){
					table = table + "<td bgcolor=\"#FF9900\">Raw" + colSeparator;
				}
				else if(status.equals(notInspectedFlag)){
					table = table + "<td bgcolor=\"#339900\">Not inspected" + colSeparator;
				}

				// Print all answers
				List<String> answers = bdr.getAnswers();
				
				if(answers!=null){
					int i = 1;
					for(String answer : answers){
						if(answer != null){
							// Replace Java newline with HTML newline
							answer.replaceAll("\n", "<br/>");
							
							// Append answer
							table = table + answer;
							
							// Don't append a colSeparator after last answer
							if(i != answers.size())
								table = table + colSeparator;
						}
						i++;
					}
				}
				
				// End the current row
				table = table + rowSuffix;
			}
			
			// End the table
			table = table + "</table>";

			return table;
	}

}
